﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;

namespace Agent.Messaging
{
  /// <summary>
  /// Class for receiving messages sent from the <see cref="MessageSender"/> class, typically across 
  /// different processes.
  /// </summary>
  /// <remarks>
  /// This class is thread safe
  /// TODO: This class (and the sender) should ideally be able to send messages of type T, where
  /// T is any kind of data (not just a string)
  /// </remarks>
  public class MessageReceiver : IDisposable
  {
    #region fields
    StateMachineHelper<SenderReceiverStateMachineState> stateMachine;

    internal const string MUTEX_SUFFIX = "_channel_mutex";
    internal const string QUEUE_SUFFIX = "_q";
    Mutex channelMutex;
    string channelName;

    const int MAX_WAITS_FOR_SENDER = 3;
    const int WAIT_FOR_SENDER_DISCONNECT_TIMEOUT_MS = 200;
    const int WAIT_FOR_SENDER_CONNECT_TIMEOUT_MS = 1000;

    MessageQueue queue;
    ManualResetEvent exitWaitLoopSignal = null;
    ManualResetEvent waitLoopTerminatedSignal = null;
    Thread waitLoopThread = null;
    DetailedLogger logger;
    SenderReceiverStateMachineState waitThreadExitState;

    static object staticLockObject = typeof(MessageReceiver);

    int instanceId;
    static int nextThreadId = 0;
    static int nextInstanceId = 0;

    static Dictionary<SenderReceiverStateMachineState, IEnumerable<SenderReceiverStateMachineState>> allowedTransitions;
    static IEnumerable<SenderReceiverStateMachineState> sinkStates;
    #endregion

    /// <summary>
    /// Sets up the state machine transitions
    /// </summary>
    static MessageReceiver()
    {
      allowedTransitions = new Dictionary<SenderReceiverStateMachineState, IEnumerable<SenderReceiverStateMachineState>>();
      allowedTransitions[SenderReceiverStateMachineState.Uninitialized] = new[] { SenderReceiverStateMachineState.Initialized };
      allowedTransitions[SenderReceiverStateMachineState.Initialized] = new[] { SenderReceiverStateMachineState.Starting };
      allowedTransitions[SenderReceiverStateMachineState.Starting] = new[] { SenderReceiverStateMachineState.Running };
      allowedTransitions[SenderReceiverStateMachineState.Running] = new[] { SenderReceiverStateMachineState.Disconnected };
      allowedTransitions[SenderReceiverStateMachineState.Disconnected] = new[] { SenderReceiverStateMachineState.Initialized, SenderReceiverStateMachineState.Starting };
      allowedTransitions[SenderReceiverStateMachineState.Stopping] = new[] { SenderReceiverStateMachineState.Initialized };

      sinkStates = new[] { SenderReceiverStateMachineState.Stopping, SenderReceiverStateMachineState.Disposed, SenderReceiverStateMachineState.Error };
    }

    /// <summary>
    /// Creates a new MessageReceiver object
    /// </summary>
    /// <param name="channelName">The name of the message channel</param>
    /// <remarks>
    /// This name must be the same name that is used by the <see cref="MessageSender"/> class
    /// </remarks>
    public MessageReceiver(string channelName)
    {
      lock (staticLockObject)
      {
        instanceId = nextInstanceId++;
      }

      stateMachine = new StateMachineHelper<SenderReceiverStateMachineState>(allowedTransitions, sinkStates, SenderReceiverStateMachineState.Uninitialized, true);

      this.channelName = channelName;
      logger = new DetailedLogger(channelName + "_" + instanceId);
      logger.Log("Starting up... ");

      try
      {
        channelMutex = new Mutex(false, channelName + MUTEX_SUFFIX);
        queue = MessageQueue.OpenQueue(channelName + QUEUE_SUFFIX);
        exitWaitLoopSignal = new ManualResetEvent(false);
        waitLoopTerminatedSignal = new ManualResetEvent(false);
      }
      catch (Exception ex)
      {
        logger.Log("Receiver hit exception " + ex.Message + " during construction... ", LoggerSeverity.Error);
        CleanupResources();
        stateMachine.GoToState(SenderReceiverStateMachineState.Error);
        throw;
      }

      stateMachine.GoToState(SenderReceiverStateMachineState.Initialized);

      logger.Log("Constructed OK");
    }

    /// <summary>
    /// Cleans up the resources in the case of error / shutdown
    /// </summary>
    private void CleanupResources()
    {
      if (logger != null && stateMachine.IsInState(SenderReceiverStateMachineState.Disposed) != true)
        logger.Log("Cleaning up...");

      if (channelMutex != null)
        channelMutex.Dispose();
      channelMutex = null;

      if (queue != null)
        queue.Dispose();
      queue = null;

      if (waitLoopThread != null)
        waitLoopThread = null;

      if (exitWaitLoopSignal != null)
        exitWaitLoopSignal.Dispose();
      exitWaitLoopSignal = null;

      if (waitLoopTerminatedSignal != null)
        waitLoopTerminatedSignal.Dispose();
      waitLoopTerminatedSignal = null;
    }

    /// <summary>
    /// Starts the receiver, making it possible to receive messages via the <see cref="MessageReceived" /> event
    /// </summary>
    public void Start()
    {
      logger.Log("Starting; current state is " + stateMachine.CurrentState);

      lock (this)
      {
        // OK to call start multiple times; it's a no-op. Also gate with CanGoToState in case we change the allowed transitions
        if (stateMachine.CanGoToState(SenderReceiverStateMachineState.Starting) && stateMachine.IsInState(new[] { SenderReceiverStateMachineState.Starting, SenderReceiverStateMachineState.Running }))
          return;

        stateMachine.GoToState(SenderReceiverStateMachineState.Starting);
      }

      if (waitLoopThread == null || waitLoopThread.IsAlive == false)
      {
        logger.Log("Creating main wait thread object");
        waitLoopThread = new Thread(WaitForMessages);
      }

      logger.Log("Kicking off the main wait thread");
      exitWaitLoopSignal.Reset();
      waitLoopTerminatedSignal.Reset();
      waitThreadExitState = SenderReceiverStateMachineState.Running;
      waitLoopThread.Start();
      new Thread(WaitThreadExitHandler).Start();
    }

    /// <summary>
    /// Method that determines what to do when the main receiving loop terminates
    /// </summary>
    /// <param name="state">Dummy parameter</param>
    /// <remarks>
    /// This method waits for the thread running <see cref="WaitForMessages" /> to end, then decides
    /// which state to transition to based on the exit status of that thread
    /// </remarks>
    void WaitThreadExitHandler(object state)
    {
      lock (staticLockObject)
      {
        Thread.CurrentThread.Name = "loop exit handler " + nextThreadId++;
      }

      logger.Log("Waiting for main wait loop to terminate... joining " + waitLoopThread.Name);
      waitLoopThread.Join();
      logger.Log("Joined! Current state is " + stateMachine.CurrentState + " and thread exit state was " + waitThreadExitState);

      lock (this)
      {
        switch (waitThreadExitState)
        {
          case SenderReceiverStateMachineState.Error:
            logger.Log("Thread ended with an error, going to error state", LoggerSeverity.Warning);
            stateMachine.GoToState(SenderReceiverStateMachineState.Error);
            break;

          case SenderReceiverStateMachineState.Stopping:
            logger.Log("Thread ended because it was asked to stop, going to initialized state", LoggerSeverity.Trace);
            stateMachine.GoToState(SenderReceiverStateMachineState.Initialized);
            break;

          case SenderReceiverStateMachineState.Disconnected:
            logger.Log("Thread ended because the sender disconnected, going to disconnected state", LoggerSeverity.Trace);
            stateMachine.GoToState(SenderReceiverStateMachineState.Disconnected);
            break;

          default:
            logger.Log("Unknown thread exit value " + waitThreadExitState, LoggerSeverity.Error);
            Debug.Assert(false, "Unknown thread exit value " + waitThreadExitState);
            break;
        }
      }

      // Call user code outside of the lock (above)
      switch (waitThreadExitState)
      {
        case SenderReceiverStateMachineState.Error:
          RaiseFailed();
          break;

        case SenderReceiverStateMachineState.Stopping:
          RaiseStopped();
          break;

        case SenderReceiverStateMachineState.Disconnected:
          RaiseSenderDisconnected();
          break;
      }

      waitLoopTerminatedSignal.Set();
      logger.Log("Thread is exiting");
    }

    /// <summary>
    /// Signals the receiver to stop listening, but returns immediately
    /// </summary>
    /// <remarks>
    /// The listening thread will finish asynchronously
    /// </remarks>
    public void StopAsync()
    {
      Stop(false);
    }

    /// <summary>
    /// Signals the receiver to stop listening, and waits for it to complete
    /// </summary>
    public void Stop()
    {
      Stop(true);
    }

    /// <summary>
    /// Signals the receiver to stop, optionally waiting for it to complete
    /// </summary>
    /// <param name="waitUntilStopped">true to wait for the thread to exit; false otherwise</param>
    void Stop(bool waitUntilStopped)
    {
      logger.Log("Stopping... wait=" + waitUntilStopped);

      if (Thread.CurrentThread == waitLoopThread)
      {
        logger.Log("Can't call from the main loop thread; will deadlock", LoggerSeverity.Error);
        Debug.Assert(false, "Can't call Stop from the main loop thread; will deadlock");
      }

      lock (this)
      {
        // All these states are no-ops; also gate with CanGoToState in case we change the rules
        if (stateMachine.CanGoToState(SenderReceiverStateMachineState.Stopping) && stateMachine.IsInState(new[] { SenderReceiverStateMachineState.Stopping, SenderReceiverStateMachineState.Uninitialized, SenderReceiverStateMachineState.Initialized, SenderReceiverStateMachineState.Disconnected }))
          return;

        if (waitLoopThread.IsAlive)
          stateMachine.GoToState(SenderReceiverStateMachineState.Stopping);
      }

      logger.Log("Setting event to let main loop know it should stop" + waitLoopThread.Name);
      exitWaitLoopSignal.Set();

      if (waitUntilStopped)
      {
        logger.Log("Waiting until the thread stopped event is set...");
        waitLoopTerminatedSignal.WaitOne();
        logger.Log(".. and done waiting!");
      }
    }

    /// <summary>
    /// The main wait loop
    /// </summary>
    /// <remarks>
    /// This method runs an infinite loop, waiting for messages to arrive and then raising the 
    /// event to let clients know that a message is waiting. The loop terminates if the object
    /// is being disposed, if someone calls <see cref="Stop"/>, or if we detect that the sender
    /// has disconnected
    /// </remarks>
    void WaitForMessages()
    {
      lock (staticLockObject)
      {
        Thread.CurrentThread.Name = "wait thread " + nextThreadId++;
      }

      logger.Log("Main wait thread starting");
      bool senderWasPreviouslyAlive = false;

      // No need to lock since GoToState is already threadsafe and it's illegal to go
      // to running state from anything other than starting
      stateMachine.GoToState(SenderReceiverStateMachineState.Running);
      waitThreadExitState = SenderReceiverStateMachineState.Error;

      // enter wait loop
      while (stateMachine.CurrentState == SenderReceiverStateMachineState.Running)
      {
        logger.Log("Wait Loop starts...");
        WaitHandleResult result = WaitHandleResult.Failed;

        WaitLoopCoreResult waitResult = WaitLoopCore(senderWasPreviouslyAlive);
        if (waitResult == WaitLoopCoreResult.MessagesWaiting || waitResult == WaitLoopCoreResult.SenderAlive)
        {
          CheckForMessages();
        }
        else if (waitResult == WaitLoopCoreResult.SenderDisconnected)
        {
          waitThreadExitState = SenderReceiverStateMachineState.Disconnected;
          break;
        }
        else if (waitResult == WaitLoopCoreResult.StopReceived)
        {
          waitThreadExitState = SenderReceiverStateMachineState.Stopping;
          break;
        }

        senderWasPreviouslyAlive = true;

        // Now we wait forever, or until we're asked to stop
        logger.Log("About to wait until either a message is received or we're told to stop");
        int handleIndex = -1;
        result = MutexExtensions.SafeWaitAny(new WaitHandle[] { channelMutex, exitWaitLoopSignal }, Timeout.Infinite, out handleIndex);
        if (result != WaitHandleResult.Acquired)
        {
          logger.Log("Couldn't acquire either handle (" + result + ")", LoggerSeverity.Error);
          Debug.Assert(false, "Couldn't acquire handles for waiting!");
          break;
        }
        else if (handleIndex != 0)
        {
          logger.Log("Told to stop listening while waiting for a message to arrive; bailing", LoggerSeverity.Information);
          waitThreadExitState = SenderReceiverStateMachineState.Stopping;
          break;
        }

        // release the mutex so the sender can acquire it again
        channelMutex.SafeReleaseMutex();
      }

      logger.Log("Thread is exiting with code " + waitThreadExitState);
    }

    /// <summary>
    /// Core logic used inside the <see cref="WaitForMessages" /> loop that checks for messages
    /// or for the sender to be disconnected
    /// </summary>
    /// <param name="senderPreviouslyAlive">true if the sender was previously alive; false otherwise</param>
    /// <returns>The result of the wait</returns>
    /// <remarks>
    /// This method is necessary because WP doesn't implement named Events that can be used
    /// across processes. Instead we have to implement a polling mechanism to see if the sender is "alive"
    /// and then wait for it to signal us by "pulsing" the mutex.
    /// The basic logic is to try and acquire the shared mutex. If we can acquire the mutex, it means
    /// that either:
    /// 
    /// 1) The sender has not started yet; we keep waiting for it
    /// 
    /// 2) The sender has posted a message for us to retrieve; we retrieve it
    /// 
    /// 3) The sender has exited; we stop waiting
    /// 
    /// TODO: Implement a simulated Event that exposes the same interface as a named event
    /// but uses polling under the covers; this should simplify the actual wait logic.
    /// </remarks>
    WaitLoopCoreResult WaitLoopCore(bool senderPreviouslyAlive)
    {
      logger.Log("Waiting for sender to first come on-line / go off-line");
      int waitCount = 0;

      while (true)
      {
        logger.Log("Top of loop");

        // Probe to see if we can get the mutex straight away
        WaitHandleResult result = WaitHandleResult.Failed;
        result = channelMutex.SafeWaitOne(0);
        if (result == WaitHandleResult.Acquired)
        {
          channelMutex.ReleaseMutex();
        }

        // If we didn't get the mutex immediately, that means the other side is alive and well
        if (result == WaitHandleResult.TimedOut)
        {
          logger.Log("Didn't get the mutex right away; that means the other end is alive");
          return WaitLoopCoreResult.SenderAlive;
        }

        // If new messages are available, that means the other side is alive and well
        if (queue.AreMessagesAvailable())
        {
          logger.Log("Got the mutex right away and there are waiting messages; that means the other end is alive");
          return WaitLoopCoreResult.MessagesWaiting;
        }

        // No messages; that means the other side isn't alive. The question is whether it was EVER alive or
        // whether it is yet to start
        waitCount++;
        if (senderPreviouslyAlive && waitCount >= MAX_WAITS_FOR_SENDER)
        {
          logger.Log("Got the mutex right away, no messages waiting, and we previously had messages; that means the sender has died");
          return WaitLoopCoreResult.SenderDisconnected;
        }

        // Sleep longer if we haven't heard from the sender yet; shorter if we have heard before
        int sleepMs = senderPreviouslyAlive ? WAIT_FOR_SENDER_DISCONNECT_TIMEOUT_MS : WAIT_FOR_SENDER_CONNECT_TIMEOUT_MS;

        // Now wait for a little bit before probing again
        logger.Log("Sleeping and will attempt to get mutex again... ZZZzzzzzzz [" + sleepMs + "ms]");
        DateTime beforeSleep = DateTime.Now;

        result = exitWaitLoopSignal.SafeWaitOne(sleepMs);

        // Note that sometimes the thread would sleep for a LONG time (as in, several orders of magnitude longer
        // than it was supposed to!); I think this is fixed in later builds
        double sleepTime = (DateTime.Now - beforeSleep).TotalMilliseconds;
        if (sleepTime > 1.1 * sleepMs)
          logger.Log("SLEPT FOR TOO LONG! Expected=" + sleepMs + ", actual=" + sleepTime, LoggerSeverity.Error);

        if (result != WaitHandleResult.TimedOut)
        {
          logger.Log("We've been told to stop listening while waiting for sender to start; breaking from loop", LoggerSeverity.Information);
          return WaitLoopCoreResult.StopReceived;
        }

        logger.Log("Going to continue the loop again");
      }

      logger.Log("Fell off the end of the loop; this isn't supposed to happen", LoggerSeverity.Error);
      Debug.Assert(false, "Fell off end of the loop!");
    }

    /// <summary>
    /// Checks for messages and alerts clients via the <see cref="MessageReceived"/> event
    /// </summary>
    /// <returns>The number of messages received</returns>
    int CheckForMessages()
    {
      // Ensure we are only ever called on the main wait thread, which means we don't
      // have to do any locking elsewhere
      if (Thread.CurrentThread != waitLoopThread)
      {
        logger.Log("Called on the wrong thread " + Thread.CurrentThread.Name + ", expecting " + waitLoopThread.Name, LoggerSeverity.Error);
        Debug.Assert(false, "Can't call CheckForMessages on any thread other than the main wait thread");
      }

      int i = 0;
      string[] messages = null;

      // This is thread safe for the queue
      messages = queue.DequeueAllMessages();

      if (messages != null && messages.Length > 0)
      {
        logger.Log("Found " + messages.Length + " messages; going to raise events");
        foreach (string s in messages)
        {
          i++;
          logger.Log("Got message '" + s + "'; about to send it onwards", LoggerSeverity.Information);
          RaiseMessageReceived(s);
        }
      }
      else
        logger.Log("No messages were found :-(");

      return i;
    }

    /// <summary>
    /// Helper to raise the <see cref="SenderDisconnected"/> event
    /// </summary>
    void RaiseSenderDisconnected()
    {
      logger.Log("Raising the SenderDisconnected event; current state = " + stateMachine.CurrentState);
      var handler = SenderDisconnected;
      if (handler != null)
        handler(this, EventArgs.Empty);
    }

    /// <summary>
    /// Helper to raise the <see cref="MessageReceived"/> event
    /// </summary>
    void RaiseMessageReceived(string message)
    {
      logger.Log("Raising the MessageReceived event; current state = " + stateMachine.CurrentState);
      var handler = MessageReceived;
      if (handler != null)
        handler(this, new MessageReceivedEventArgs(message));
    }

    /// <summary>
    /// Helper to raise the <see cref="Failed"/> event
    /// </summary>
    void RaiseFailed()
    {
      logger.Log("Raising the Failed event; current state = " + stateMachine.CurrentState);
      var handler = Failed;
      if (handler != null)
        handler(this, EventArgs.Empty);
    }

    /// <summary>
    /// Helper to raise the <see cref="Stopped"/> event
    /// </summary>
    void RaiseStopped()
    {
      logger.Log("Raising the Stopped event; current state = " + stateMachine.CurrentState);
      var handler = Stopped;
      if (handler != null)
        handler(this, EventArgs.Empty);
    }

    /// <summary>
    /// Raised when a message is received from the sender. A single message is delivered at a time
    /// </summary>
    /// <remarks>
    /// This event is raised on a background thread
    /// </remarks>
    public event EventHandler<MessageReceivedEventArgs> MessageReceived;

    /// <summary>
    /// Raised when the sender has disconnected; the receiver will stop itself in this case
    /// </summary>
    /// <remarks>
    /// This event is raised on a background thread
    /// </remarks>
    public event EventHandler SenderDisconnected;

    /// <summary>
    /// Raised when the receiver fails for any reason (eg, can't acquire system resources)
    /// </summary>
    /// <remarks>
    /// This event might be raised on a background thread
    /// </remarks>
    public event EventHandler Failed;

    /// <summary>
    /// Raised when the receiver has successfully stopped
    /// </summary>
    /// <remarks>
    /// This event might be raised on a background thread
    /// </remarks>
    public event EventHandler Stopped;

    /// <summary>
    /// Disposes of the object, releasing any resources
    /// </summary>
    public void Dispose()
    {
      Dispose(false);
      GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Disposes of the object, releasing any resources
    /// </summary>
    /// <param name="isFinalizing">Whether the object is being finalized</param>
    void Dispose(bool isFinalizing)
    {
      logger.Log("isFinalizing = " + isFinalizing + ", currentState = " + stateMachine.CurrentState);
      if (isFinalizing)
        logger.Log("Exceptions are expected if some items are already disposed...");

      lock (this)
      {
        if (stateMachine.CurrentState == SenderReceiverStateMachineState.Disposed)
        {
          logger.Log("Already disposing or disposed; returning immediately");
          return;
        }

        logger.Log("Setting isDisposed flag so nobody else tries to dispose the object");

        stateMachine.GoToState(SenderReceiverStateMachineState.Disposed);
      }

      try
      {
        Stop();
      }
      catch (ObjectDisposedException)
      {
        logger.Log("Exception trying to set the stop listening event because it is already disposed",
          isFinalizing ? LoggerSeverity.Information : LoggerSeverity.Error);
      }

      CleanupResources();
    }

    /// <summary>
    /// Finalizer that calls <see cref="Dispose"/>
    /// </summary>
    ~MessageReceiver()
    {
      logger.Log("Finalizing...");
      Dispose(true);
    }

    /// <summary>
    /// Returns the current state of the state machine
    /// </summary>
    public SenderReceiverStateMachineState CurrentState { get { return stateMachine.CurrentState; } }
  }
}
